<!DOCTYPE html>
<html lang="en" xml:lang="en">
<head>
    <title>OTA Update</title>
    <meta charset="UTF-8" />

    <style>
        h1 {font-size: 2em;}
        h2 {font-size: 1.5em; margin-block-start: 0.0em; margin-block-end: 0.2em;}
        h3 {font-size: 1.2em;}
        p {font-size: 1em;}
        
        input[type=file] {
            width: 660px;
            padding: 5px 0px;
            display: inline-block;
            font-size: 16px; 
        }    
        
        .button {
            padding: 5px 10px;
            width: 205px;
            font-size: 16px;
        }
    </style>

    <link href="firework.css?v=$COMMIT_HASH" rel="stylesheet">
    <script src="md5.min.js?v=$COMMIT_HASH"></script>
    <script type="text/javascript" src="jquery-3.6.0.min.js?v=$COMMIT_HASH"></script>
    <script type="text/javascript" src="common.js?v=$COMMIT_HASH"></script>
    <script type="text/javascript" src="firework.js?v=$COMMIT_HASH"></script>
</head>

<body style="font-family: arial; padding: 0px 10px;">
    <h2>OTA Update</h2>
    <p>Check the <a href="https://github.com/jomjol/AI-on-the-edge-device/releases" target=_blank>Release Page</a> to see if there is an update available. </p>
    <p>You can also automatically get notified about a release, see <a href="https://jomjol.github.io/AI-on-the-edge-device-docs/New-Releases-Notification" target=_blank>Release Notifications</a>.</p>

    <h3>Update</h3>
    On the <a href="https://github.com/jomjol/AI-on-the-edge-device/releases" target=_blank>Release Page</a>, pick the <i><span style="font-family:monospace">AI-on-the-edge-device__update__*.zip</span></i> file!</p>
    <p>Alternatively the following file types are supported:</p>
    <ul>
        <li><span style="font-family:monospace">*.zip</span> &rarr; Gets unpacked on the SD-Card (most top folder)</li>
        <li><span style="font-family:monospace">*.bin</span> &rarr; gets installed onto the ESP32s program flash</li>
        <li><span style="font-family:monospace">*.tfl/tflite</span> &rarr; Gets copied to your <i>SD-Card/config</i></li>
    </ul>

    <p><b>Do not reload this page or switch to another page while the update is in progress!</b><br></p>

    <form id="upload_form" enctype="multipart/form-data" method="post">
        <input type="file" accept=".bin,.zip,.tfl,.tflite" name="file_selector" id="file_selector" onchange="validate_file()"><br><br>

        <button class="button" id="start_OTA_button" type="button" onclick="start_OTA()" disabled>Upload And Install</button>
        <br><br>
        <progress id="progressBar" value="0" max="100" style="width:600px;"></progress>
        <h3><span id="status">Status: Idle</span></h3>
        <p id="loaded_n_total"></p>
    </form>
    


    <script language="JavaScript">
        var domainname = getDomainname();
        
        var action_runtime = 0;
        
        /* Max size of an individual file. Make sure this
        * value is same as that set in server_file.c */
        var MAX_FILE_SIZE = 8000*1024;
        var MAX_FILE_SIZE_STR = "8MB";
        
        function validate_file() {
            document.getElementById("start_OTA_button").disabled = true;

            var fileInput = document.getElementById("file_selector").files;
            var filepath = document.getElementById("file_selector").value;

            console.log("filepath: " + filepath);

            filename = filepath.split(/[\\\/]/).pop();

            console.log("filename: " + filename);

            /* Various checks on filename length and file size */
            if (fileInput.length == 0) {
                firework.launch('No file selected!', 'danger', 30000);
                return;
            } else if (filename.length == 0) {
                firework.launch('File path on server is not set!', 'danger', 30000);
                return;
            } else if (filename.length > 100) {
                firework.launch('Filename is too long! Max 100 characters.', 'danger', 30000);
                return;
            } else if (filename.indexOf(' ') >= 0) {
                firework.launch('Filename can not have spaces!', 'danger', 30000);
                return;
            } else if (filename[filename.length-1] == '/') {
                firework.launch('Filename not specified after path!', 'danger', 30000);
                return;
            } else if (fileInput[0].size > MAX_FILE_SIZE) {
                firework.launch("File size must be less than " + MAX_FILE_SIZE_STR + "!", 'danger', 30000);
                return;
            }

            /* Check if the fillename matches our expected pattern
             *  - AI-on-the-edge-device__update__*.zip
             *  - firmware__*.bin
             *  - *.ftl
             *  - *.tflite */
            if ( /(^AI-on-the-edge-device__update__)[a-zRC0-9()_\-.]*(\.zip$)/i.test(filename) || // OK
               ( /(^AI-on-the-edge-device__firmware)[a-zRC0-9()_\-.]*(\.bin$)/i.test(filename)) ||
               ( /[a-zRC0-9()_\-.]*(\.tfl$)/i.test(filename)) ||
               ( /[a-zRC0-9()_\-.]*(\.tflite$)/i.test(filename))) {
                firework.launch('Great, the filename matches our expectations. You can now press "Upload and install".', 'success', 5000);
            }
            /* Following filenames are acceptiod but not prefered:
             * - *.bin
             * - *.zip */
            else if (filename.endsWith(".zip") || filename.endsWith(".bin")) { // Warning but still accepted
                firework.launch('The filename does not match the suggested filename pattern, but is nevertheless accepted. You can now press "Upload and install".', 'warning', 10000);
            }
            /* Any other file name format is not accepted */
            else { // invalid
                firework.launch('The filename does not match our expectations!', 'danger', 30000);
                return;
            }

            document.getElementById("start_OTA_button").disabled = false;
        }


        function start_OTA() {
            document.getElementById("start_OTA_button").disabled = true;

            var file_name = document.getElementById("file_selector").value;
            document.getElementById("file_selector").disabled = true;
            file_name = file_name.split(/[\\\/]/).pop();
            document.getElementById("status").innerText = "Status: File selected";

            prepareOnServer();
        }


        function doRebootAfterUpdate() {
            var xhttp = new XMLHttpRequest();
            xhttp.open("GET", domainname + "/reboot", true);
            xhttp.send();
        }


        function prepareOnServer() {
            document.getElementById("status").innerText = "Status: Preparing device...";

            var xhttp = new XMLHttpRequest();

            var file_name = document.getElementById("file_selector").value;
            filePath = file_name.split(/[\\\/]/).pop();

            /* first delete the old firmware AND empty the /firmware directory*/	
            xhttp.onreadystatechange = function() {
                if (xhttp.readyState == 4) {
                    if (xhttp.status == 200) {
                        /* keine Reaktion, damit sich das Dokument nicht ändert */
                        upload();
                    } else if (xhttp.status == 0) {
                        firework.launch('Server closed the connection abruptly!', 'danger', 30000);
                    } else {
                        firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
                    }
                }
            };

            var url = domainname + "/ota?task=emptyfirmwaredir";
            xhttp.open("GET", url, true);
            xhttp.send();
        }


        function validateMd5(md5_on_device, callback) {
            const reader = new FileReader();

            reader.onload = (event) => {
                const fileContent = event.target.result;
                md5_on_webbrowser = md5(fileContent);
                console.log("MD5 on device: " + md5_on_device + ", MD5 on web browser: " + md5_on_webbrowser);
                if (md5_on_device == md5_on_webbrowser) {
                    console.log("MD5 values are equal");
                    callback(true);
                }
                else {
                    console.log("MD5 values are NOT equal!");
                    callback(false);
                }
            }

            var fileInput = document.getElementById("file_selector").files;
            reader.readAsArrayBuffer(fileInput[0]);
        }


        function extract() {
            var xhttp = new XMLHttpRequest();
            /* first delete the old firmware */	
            xhttp.onreadystatechange = function() {
                if (xhttp.readyState == 4) {
                    if (xhttp.status == 200) {
                        document.cookie = "page=overview.html?v=$COMMIT_HASH" + "; path=/"; // Make sure after the reboot we go to the overview page

                        if (xhttp.responseText.startsWith("reboot")) { // Reboot required
                            console.log("The device will now reboot and install the update!");
                            document.getElementById("status").innerText = "Status: Installing...";
                            firework.launch('Upload completed and validated. The device will now reboot and install the update', 'success', 5000);
                        
                            /* Tell it to reboot */
                            doRebootAfterUpdate();

                            action_runtime = 0;
                            updateTimer = setInterval(function() {                            
                                action_runtime += 1;
                                console.log("Waiting: " + action_runtime + "s");  
                                _("progressBar").value = Math.round(action_runtime);

                                if (action_runtime > 10) { // After 10 seconds, start to check if we are up again
                                    /* Check if the device is up again and forward to index page if so */
                                    fetch('reboot_page.html?v=$COMMIT_HASH&' + Math.random(), {mode: 'no-cors'}).then(
                                        r=>{parent.location.href=('index.html?v=' + Math.random());}
                                    )
                                }

                                if (action_runtime > 100) { // We reached 300 seconds but device is not ready yet
                                    firework.launch("The device seems not do be up again, or maybe we missed it. Try to reload this page or reset the device!", 'danger', 30000);
                                    clearInterval(updateTimer);
                                }
                            }, 3000);                           
                        }
                        else // No reboot required
                        {
                            document.getElementById("status").innerText = "Status: Update completed";
                            firework.launch('Update completed!', 'success', 5000);
                            document.getElementById("file_selector").disabled = false;
                        }
                    } else if (xhttp.status == 0) {
                        firework.launch('Server closed the connection abruptly!', 'danger', 30000);
                    } else {
                        firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
                    }
                }
            };

            var file_name = document.getElementById("file_selector").value;
            filePath = file_name.split(/[\\\/]/).pop();
            var url = domainname + "/ota?task=update&file=" + filePath;
            xhttp.open("GET", url, true);
            xhttp.send();
        }


        function _(el) {
            return document.getElementById(el);
        }


        function upload() {
            document.getElementById("status").innerText = "Status: Uploading...";

            var url = domainname + "/upload/firmware/" + filePath + "?md5";

            var file = _("file_selector").files[0];
            var formdata = new FormData();
            formdata.append("file_selector", file);
            var ajax = new XMLHttpRequest();
            ajax.upload.addEventListener("progress", progressHandler, false);
            ajax.addEventListener("load", completeHandler, false);
            ajax.addEventListener("error", errorHandler, false);
            ajax.addEventListener("abort", abortHandler, false);

            ajax.open("POST", url);
            ajax.send(file);
        }


        function progressHandler(event) {
            _("loaded_n_total").innerHTML = "Uploaded " + (event.loaded / 1024 / 1024).toFixed(2) + 
                    " MB of " + (event.total / 1024/ 1024).toFixed(2) + " MB";
            var percent = (event.loaded / event.total) * 100;
            _("progressBar").value = Math.round(percent);

            if (Math.round(percent) == 100) {
                _("progressBar").value = 0; //will clear progress bar after successful upload
                _("loaded_n_total").innerHTML = "";
                _("status").innerHTML = "Status: Upload completed. Validating file...";
            }
            else {
                _("status").innerHTML = "Status: " + Math.round(percent) + "% uploaded...";
            }
        }


        function completeHandler(event) {
            console.log("Upload completed");
            console.log("Response: " + event.target.responseText);

            try {
                md5_on_device = JSON.parse(event.target.responseText).md5;
                validateMd5(md5_on_device, (result) => {
                    if (result == true) {
                        _("status").innerHTML = "Status: The uploaded file is valid, installing it...";
                        extract();
                    }
                    else {
                        _("status").innerHTML = "Status: The file got corrupted! Please upload it again!";
                        firework.launch('Upload failed, the file got corrupted! Please upload it again!', 'danger', 30000);
                        document.getElementById("start_OTA_button").disabled = false;
                    }
                });
            }
            catch (e) {
                // If the firmware is to old, it will return the file sever page instead of the JSON object with the MD5 sum.
                // In juch case just proceed to keep legacy support.
                console.log("It seems to be a legacy firmware, installing the update without validation!");
                _("status").innerHTML = "Status: It seems to be a legacy firmware, installing the update without validation...";
                extract();
            }
        }


        function errorHandler(event) {
            _("status").innerHTML = "Status: Upload Failed";
            firework.launch('Upload failed!', 'danger', 30000);
            document.getElementById("file_selector").disabled = false;
        }


        function abortHandler(event) {
            _("status").innerHTML = "Status: Upload Aborted";
            firework.launch('Upload aborted!', 'danger', 30000);
            document.getElementById("file_selector").disabled = false;
        }
    </script>
</body>
</html> 
